Convert Search footer totals via GetTransactionsConvertedAmount command#94669
Convert Search footer totals via GetTransactionsConvertedAmount command#94669c3024 wants to merge 31 commits into
Conversation
Replace the auxiliary-snapshot footer-currency conversion with the dedicated GetTransactionsConvertedAmount Auth command. The command merges converted figures into a new searchFooterConversion Onyx cache (by transaction and by query hash, each nested under the target currency) via onyxData, leaving the live search snapshot in its original currency. The footer reads converted totals from the cache and fetches coverage on demand only when the cache is missing what it needs, so the picker stays enabled for partial selections instead of being disabled when a selection falls outside snapshot coverage. Removes the now-obsolete buildFlatQueryWithoutGroupBy helper and the search() targetCurrency param.
…transactions-converted-amount
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
- Drop unused ConvertedAmountByCurrency/ConvertedTotalByCurrency exports - Remove the always-true currentSearchHash undefined guard - Collapse getObjectMember/getNumberMember into one getGroupCount helper - Drop the live-refresh custom-currency reset (kept the group-selection reset)
Converted footer totals are ephemeral, session-scoped display data, so drop the SEARCH_FOOTER_CONVERSION cache when the Search screen unmounts rather than persisting it across sessions. Matches the issue's reset-on-navigate-away semantics and bounds the cache to a single Search session.
On the Reports search (type:expense-report), convert a selection by report
instead of by transaction: derive the selected report IDs, read/sum their
converted totals from the new SEARCH_FOOTER_CONVERSION `reports` cache section,
and fetch uncovered report IDs via getFooterConvertedAmounts({reportIDList}).
Also stop showing the footer skeleton for a client-side subtotal (partial
selection) while the whole-search total is still loading, and only show the
conversion skeleton when there are IDs to fetch.
Selecting every loaded report made selectedExpenseCount === metadataCount, which routed the footer to the whole-search query path — invalid on the Reports search, where the backend returns report rows (no flat per-transaction rows) and errored with "No column named groupAmount". Now any explicit (loaded) report selection, including when all loaded reports are selected, converts via the per-report path. The whole-search query path is skipped on the Reports search, falling back to the default total instead of erroring or showing a stuck skeleton.
Enable the footer currency picker for grouped searches. A grouped search converts its selection by group key (new SEARCH_FOOTER_CONVERSION `groups` section, summed like reports), and the picker stays enabled on grouped views. The whole-search grand total now converts for aggregate (report/grouped) searches with nothing selected too, resolving from the cached searchTotals that every search type now returns — so the picker is no longer inert there.
A grouped search can select whole groups and individual transactions from other groups at once. Convert per row instead of treating the selection as groups only: whole-group entries from the `groups` cache, individual transactions from the `transactions` cache, summed together (reports stay aggregate-by-report). The effect fetches both the grouped totals and the loose transactions as needed. Since the whole-search grand total is now converted for every search type, all-selected falls to that grand total, so the all-selected-aggregate special case is dropped (shouldUseClientTotal = !metadataCount || hasPartialSelection).
…transactions-converted-amount
…amount # Conflicts: # src/components/Button/index.tsx # src/components/Search/FilterDropdowns/DisplayPopup.tsx # src/components/Search/FilterDropdowns/GroupCurrencyPopup.tsx # src/components/Search/SearchPageFooter.tsx # src/components/Search/SearchSelectionFooter.tsx # src/libs/actions/Search.ts
…transactions-converted-amount # Conflicts: # src/components/ButtonComposed/composed/ButtonWithIcons.tsx # src/components/Search/FilterComponents/SingleSelect.tsx # src/components/Search/SearchPageFooter.tsx # src/components/Search/SearchSelectionFooter.tsx
…transactions-converted-amount
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c852c69a6e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…detection Instead of writing the source stamp optimistically before the read returns (which briefly, or indefinitely on a dropped/failed read, makes a stale conversion look fresh), send the stamps to GetTransactionsConvertedAmount so it echoes them back in the same onyxData merge as the converted values. The stamp and its value are then written together and a stale entry is never shown as fresh.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3545772dc0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
No conversion can complete offline, so an offline edit or selection change that leaves a conversion missing/stale should not flip the footer into the loading skeleton (which would stay until connectivity returns). Guard the converting state and the fetch effect with isOffline; the effect re-runs on reconnect.
A failed read leaves no converted value, so the freshness check never passes and the footer would show a loading skeleton indefinitely. Record the failed target currency (cleared when a fresh request for it is issued) and drop the converting state, falling back to the default total; the effect still retries on an edit, currency change, or reconnect.
Drops the backend source-stamp echo (Auth/Web follow-ups descoped): the App stamps sources optimistically again (in the read's optimisticData) instead of sending them to the command. Keeps the App-only offline guard and the read-failure fallback (fall back to the default total instead of a stuck skeleton or a stale converted value).
…transactions-converted-amount # Conflicts: # src/ONYXKEYS.ts # src/libs/ExportOnyxState/common.ts
The Onyx errors convention (OnyxCommon Errors) is a displayable message map; this field is a silent per-currency failure flag, so name it for what it is.
Group rows only render on grouped expense/trip searches, where the groups conversion path handles them, so a selected group can never persistently meet a non-grouped footer. The one-frame window during a grouping switch (selection clears in an effect) is already inert because a hash change resets selectedCurrency. Drops the guard, the currency reset, and the shouldAllowCurrencyChange prop whose false branch is now unreachable.
The backend alters what a query matches based on searchKey (e.g. unapprovedCash excludes card expenses), so a conversion request without it can cover a different expense set than the snapshot — on suggested tabs like Cash accruals the footer count changed when switching currency. Serialize searchKey into the conversion jsonQuery exactly as search() does.
|
Tested all the bugs raised by reviewers and QA on the last PR and verified that all work as expected. I couldn't reproduce these two issues:
Even if they are reproducible, I don't think either of them is a blocker. This issue, #94531, is caused by the forex rate conversion and it is unrelated to this PR. This need not block deployment because the reproduction steps require creating an expense with a zero amount. |
|
@gijoe0295 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
|
Sorry, @gijoe0295. The reverted PR was reviewed by @ahmedGaber93, so I think he'll review this one as well. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: db800fb57c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: db800fb57c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
- Share search()'s backend-query transform (client-only field stripping and the client-side-sort date fallback) with getFooterConvertedAmounts via a new getBackendQueryJSON helper, so conversion requests describe the same query. - Restore the numeric-total guard around the footer total picker. - Memoize selectedTransactionsKeys so the conversion-fetch effect's dependencies keep stable references. - Document shouldUseFixedPopoverHeight, name the skeleton bar gap, and add a header comment to CurrencyPopup.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c44fedf6a2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Explanation of Change
Re-implements the Search footer currency picker on a dedicated read command,
GetTransactionsConvertedAmount(agreed in Expensify #651977), replacing the auxiliary-snapshot approach from #90777 (reverted in #94562) whose first-page-only coverage forced the picker to disable for out-of-page selections. The command reusesSearch's query/aggregation (so numbers match) and converts on demand — whole search, selected transactions (transactionIDList), selected reports (reportIDList), or a grouped view's groups + grand total — merging results into a newsearchFooterConversionOnyx cache without touching the live snapshot. The footer reads that cache (fetching only what's missing), converts per row so a grouped selection can mix groups and loose transactions, works with nothing selected, and fetches again when an inline edit moves a stamped source value.Backend (both merged): Auth #22515, Web #54039.
Fixed Issues
$ https://github.com/Expensify/Expensify/issues/634354
https://github.com/Expensify/Expensify/issues/651977
PROPOSAL:
Tests
Offline tests
QA Steps
Same as tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)Avatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
Screen.Recording.2026-07-22.at.2.37.13.PM.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-07-22.at.12.29.12.PM.mov
Screen.Recording.2026-07-22.at.12.20.11.PM.mov
Screen.Recording.2026-07-22.at.12.13.13.PM.mov
Screen.Recording.2026-07-22.at.12.11.55.PM.mov
Screen.Recording.2026-07-22.at.12.10.36.PM.mov
Screen.Recording.2026-07-22.at.12.00.34.PM.mov
Screen.Recording.2026-07-22.at.11.57.59.AM.mov
Screen.Recording.2026-07-21.at.6.18.03.PM.mov
Screen.Recording.2026-07-21.at.6.15.52.PM.mov
Screen.Recording.2026-07-21.at.5.57.00.PM.mov
Screen.Recording.2026-07-21.at.5.51.10.PM.mov
Screen.Recording.2026-07-21.at.5.45.25.PM.mov